using System.Collections.Generic;
using System.IO;
using System;
using Pretorianie.Sangraal.Providers.Data;

namespace Pretorianie.Sangraal.Providers
{
    /// <summary>
    /// FolderInfoProvider class provides detailed info about specified folder and all of its
    /// files and subdirectories. It also introduces cache mechanisms that can
    /// reduce a number of calls to the system. Special monitor is created to guarantee
    /// that external modifications of folder structure will have also an impact on
    /// the data given by FolderInfoProvider.
    /// </summary>
    public class FolderInfoProvider
    {
        private readonly FileSystemWatcher fileWatcher;
        private readonly Dictionary<string, IList<FolderItemInfo>> infos;
        private readonly object getInfoLock;
        private readonly IFolderAttributesDescriptor attrProvider;
        private string startPath;

        /// <summary>
        /// Init constructor.
        /// </summary>
        public FolderInfoProvider(IFolderAttributesDescriptor p)
        {
            // create lock and cache objects:
            getInfoLock = new object();
            infos = new Dictionary<string, IList<FolderItemInfo>>();
            attrProvider = p;

            // setup the file watcher to monitor changes
            // of folder structure:
            fileWatcher = new FileSystemWatcher();
            fileWatcher.NotifyFilter = NotifyFilters.DirectoryName | NotifyFilters.FileName;
            fileWatcher.Filter = "*.*";
            fileWatcher.IncludeSubdirectories = true;

            fileWatcher.Created += ItemCreatedDeleted;
            fileWatcher.Deleted += ItemCreatedDeleted;
            fileWatcher.Renamed += ItemRenamed;
        }

        /// <summary>
        /// Default constructor.
        /// It will support only the most common type of files.
        /// </summary>
        public FolderInfoProvider()
            : this(new CommonFolderAttributeProvider())
        {
        }

        #region Internal Event Handlers

        void ItemRenamed(object sender, RenamedEventArgs e)
        {
            string path = null;

            // is a directory ?
            if (Directory.Exists(e.FullPath))
            {
                DirectoryInfo dirInfo = Directory.GetParent(e.FullPath);

                if (dirInfo != null)
                    path = dirInfo.FullName.ToLower();
            }
            else
            {
                // or get parent directory for current file:
                path = Path.GetFullPath(e.FullPath).ToLower();
            }

            InvalidatePath(path);
        }

        void ItemCreatedDeleted(object sender, FileSystemEventArgs e)
        {
            string path = Path.GetFullPath(e.FullPath).ToLower();
            string lowerPath = e.FullPath.ToLower();

            // is a directory ?
            if (path == lowerPath)
            {
                DirectoryInfo dirInfo = Directory.GetParent(path);
                path = dirInfo != null ? dirInfo.FullName.ToLower() : null;
            }

            InvalidatePath(path);
        }

        #endregion

        private void InvalidatePath(string path)
        {
            lock (infos)
            {
                infos.Remove(path);
            }
        }

        private IList<FolderItemInfo> GetRawFolderInfo(string path)
        {
            IList<FolderItemInfo> result = new List<FolderItemInfo>();
            DateTime dtNow = DateTime.Now;

            // get subdirectories:
            string[] subDirectories = Directory.GetDirectories(path);
            foreach (string directory in subDirectories)
            {
                string[] parts = directory.Split('\\');
                string name = parts[parts.Length - 1];

                FolderItemInfo info = new FolderItemInfo();
                ItemAttributes attrs = attrProvider.GetFolderAttributes(name.ToLower());

                // fill description of one item:
                info.Name = name;
                info.IconName = attrs.IconName;
                info.Size = 0;
                info.TypeDescription = attrs.TypeDescription;
                info.DateModified = attrProvider.GetFormattedDate(dtNow);
                info.IsFolder = true;
                info.Text = directory;
                info.Extension = string.Empty;
                info.SizeString = string.Empty;

                result.Add(info);
            }

            // get files:
            string[] files = Directory.GetFiles(path);
            foreach (string file in files)
            {
                string[] parts = file.Split('\\');
                string name = parts[parts.Length - 1];
                FileInfo fi = new FileInfo(file);
                
                ItemAttributes atts = attrProvider.GetFileAttributes(name, fi.Extension.ToLower());
                FolderItemInfo info = new FolderItemInfo();

                // generate description of one item:
                info.Name = name;
                info.IconName = atts.IconName;
                info.Size = fi.Length;
                info.TypeDescription = atts.TypeDescription;
                info.DateModified = attrProvider.GetFormattedDate(fi.LastWriteTime);
                info.IsFolder = false;
                info.Text = file;
                info.Extension = fi.Extension;
                info.SizeString = attrProvider.GetFormattedSize(fi.Length);

                result.Add(info);
            }

            return result;
        }

        /// <summary>
        /// Returns monitored folder.
        /// </summary>
        public string StartPath
        {
            get { return startPath; }
        }

        /// <summary>
        /// Starts monitoring of given folder.
        /// Since then all request of folder infos will be cached.
        /// However any changed done to given folder structure will
        /// automatically invalidate the cache.
        /// </summary>
        public void Start (string path)
        {
            if(string.IsNullOrEmpty(path))
                throw new ArgumentNullException("path", "Path can not be empty");

            // create directory if needed:
            if (!Directory.Exists(path))
                Directory.CreateDirectory(path);

            // convert path to valid one:
            startPath = Path.GetFullPath(path).ToLower();
            // path.ToLower().Replace('/', Path.DirectorySeparatorChar)
            //    .Replace('\\', Path.DirectorySeparatorChar);
            fileWatcher.EnableRaisingEvents = false;
            fileWatcher.Path = startPath;
            fileWatcher.EnableRaisingEvents = true;
        }

        /// <summary>
        /// Stops monitoring and caching file/folder infos.
        /// </summary>
        public void Stop ()
        {
            startPath = null;
            fileWatcher.EnableRaisingEvents = false;
            infos.Clear();
        }

        public IList<FolderItemInfo> GetFolderInfo(string path, out bool fromCache)
        {
            if (string.IsNullOrEmpty(path))
                throw new ArgumentNullException("path", "Path can not be empty");
            if (!path.StartsWith(startPath) || string.IsNullOrEmpty(startPath))
                throw new ArgumentOutOfRangeException("path", "Requested path is out of scope for current manager");
            if (!Directory.Exists(path))
                throw new ArgumentOutOfRangeException("path", "Specified path does not exist");

            IList<FolderItemInfo> folderInfo;
            lock (infos)
            {

                if (infos.TryGetValue(path, out folderInfo))
                {
                    fromCache = true;
                    return folderInfo;
                }
            }

            lock (getInfoLock)
            {
                folderInfo = GetRawFolderInfo(path);
                lock (infos)
                {
                    fromCache = false;
                    if (!infos.ContainsKey(path))
                        infos.Add(path, folderInfo);
                }
            }

            return folderInfo;
        }
    }
}